﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;

namespace GeoParser
{
	/// <summary>
	/// An in-memory GPX structure.
	/// </summary>
	public class GpsExchange
	{
		private readonly XDocument m_Doc;
		private readonly XmlNamespaceManager m_NamespaceManager;
		public readonly XNamespace Namespace;

		public GpsExchange()
		{
			m_Doc = new XDocument();
			m_NamespaceManager = new XmlNamespaceManager(new NameTable());

			Namespace = "http://www.topografix.com/GPX/1/1";
			m_NamespaceManager.AddNamespace("gpx", Namespace.NamespaceName);

			m_Doc.Add(new XElement(Namespace + "gpx",
				new XAttribute(XNamespace.Xmlns + "xsi", "http://www.w3.org/2001/XMLSchema-instance"),
				new XAttribute(XNamespace.Xmlns + "xsd", "http://www.w3.org/2001/XMLSchema")
				));
		}

		private GpsExchange(XDocument doc, XmlNameTable table)
		{
			m_Doc = doc;
			m_NamespaceManager = new XmlNamespaceManager(table);

			Namespace = doc.Root.Attribute("xmlns").Value;
			m_NamespaceManager.AddNamespace("gpx", Namespace.NamespaceName);
			m_NamespaceManager.AddNamespace("gs", "http://www.groundspeak.com/cache/1/0");
		}

		public static GpsExchange Load(string uri)
		{
			using (XmlReader reader = XmlReader.Create(uri))
			{
				XDocument doc = XDocument.Load(reader);
				return new GpsExchange(doc, reader.NameTable);
			}
		}

		public void Save(string fileName)
		{
			m_Doc.Save(fileName);
		}

		public string Name
		{
			get { return Find("/gps/name").Value; }
		}

		public Waypoint WayPoint(string name)
		{
			XElement element = WayPointElement(name);
			if (element != null)
				return new Waypoint(element);
			else
				return null;
		}

		private XElement WayPointElement(string name)
		{
			return Find(string.Format("/gpx:gpx/gpx:wpt[gpx:name='{0}']", name));
		}

		public IEnumerable<Waypoint> WayPoints
		{
			get
			{
				return m_Doc.XPathSelectElements("/gpx:gpx/gpx:wpt", m_NamespaceManager)
					.Select(x => new Waypoint(x));
			}
		}

		public IEnumerable<Waypoint> WayPointsMatching(string xpath)
		{
			return m_Doc.XPathSelectElements(string.Format("/gpx:gpx/gpx:wpt[{0}]", xpath), m_NamespaceManager)
				.Select(x => new Waypoint(x));
		}

		private XElement Find(string xPath)
		{
			return m_Doc.XPathSelectElement(xPath, m_NamespaceManager);
		}

		/// <summary>
		/// Merge all the waypoints from <paramref name="Other"/> into this instance.
		/// </summary>
		/// <param name="other">Another <see cref="GpsExchange"/> instance to merge from.</param>
		public void Add(GpsExchange other)
		{
			if (other == null)
				return;

			foreach (Waypoint waypoint in other.WayPoints)
			{
				if (this.WayPoint(waypoint.Name) == null)
				{
					m_Doc.Root.Add(new XElement(other.WayPointElement(waypoint.Name)));
				}
			}
		}

		public void Add(Route route)
		{
			if (route == null)
				return;

			m_Doc.Root.Add(route.ToXElement(Namespace));
		}

		public void Remove(GpsExchange other)
		{
			if (other == null)
				return;

			var toRemove = new List<Waypoint>();
			foreach (Waypoint waypoint in WayPoints)
			{
				if (other.WayPoint(waypoint.Name) != null)
					toRemove.Add(waypoint);
			}

			toRemove.ForEach(x => Remove(x));
		}

		private void Remove(Waypoint waypoint)
		{
			if (waypoint == null)
				return;

			XElement element = WayPointElement(waypoint.Name);
			if (element != null)
				element.Remove();
		}

		public void Remove(IEnumerable<Waypoint> waypoints)
		{
			if (waypoints == null)
				return;
	
			foreach (Waypoint waypoint in waypoints)
				Remove(waypoint);
		}

		public IEnumerable<Waypoint> WaypointsNear(GeoPoint centre, Distance radius)
		{
			return WayPoints.Where(x => GeoPoint.DistanceBetween(x, centre).Miles <= radius.Miles);
		}

		public IEnumerable<Waypoint> WaypointsDescribed(string desc)
		{
			return WayPoints.Where(x => x.Description.Contains(desc)).OrderBy(x => x.Description, new NaturalComparer());
		}

		public IEnumerable<Track> Tracks
		{
			get
			{
				return m_Doc.XPathSelectElements("/gpx:gpx/gpx:trk", m_NamespaceManager)
					.Select(x => new Track(x));
			}
		}
	}
}